﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

using System;
using System.Globalization;
using System.IO;
using System.Text;

namespace Tesla.Content.Loaders {
    /// <summary>
    /// String tokenizer that creates a stream of tokens from an input text source. A token is typically
    /// separated by either a white space, a semi-colon, or a control character (such as a new line). Double slash (//) comments are allowed
    /// in the text, and are skipped over. Text is considered a comment at the start of two forward slash characters,
    /// and end at the following new line character.
    /// </summary>
    internal class StringTokenizer {
        private const char EOF = (char) 0;
        private StringBuilder _token;
        private int _pos;
        private String _data;
        private String _currentToken;
        private String _peekToken;
        private int _line;
        private int _col;
        private int _savedLine;
        private int _savedCol;
        private char[] _numDelims = new char[] { 'f' };

        /// <summary>
        /// The current token, this is the result of the last NextToken() call.
        /// </summary>
        public String CurrentToken {
            get {
                return _currentToken;
            }
        }

        /// <summary>
        /// Line number of the current token.
        /// </summary>
        public int LineNumber {
            get {
                return _savedLine;
            }
        }

        /// <summary>
        /// Starting column value of the current token.
        /// </summary>
        public int Column {
            get {
                return _savedCol;
            }
        }

        /// <summary>
        /// Creates a new instance of StringTokenizer.
        /// </summary>
        /// <param name="reader">Input text</param>
        public StringTokenizer(TextReader reader) {
            _pos = 0;
            if(reader == null) {
                throw new ArgumentNullException("reader", "TextReader cannot be null");
            }
            _data = reader.ReadToEnd();
            _token = new StringBuilder();
            _currentToken = String.Empty;
            _line = 0;
            _col = 0;
        }

        /// <summary>
        /// Creates a new instance of StringTokenizer.
        /// </summary>
        /// <param name="text">Input text</param>
        public StringTokenizer(String text) {
            _pos = 0;
            if(text == null) {
                throw new ArgumentNullException("text", "Text cannot be null");
            }
            _data = text;
            _token = new StringBuilder();
            _currentToken = String.Empty;
            _line = 1;
            _col = 1;
        }

        /// <summary>
        /// Creates a new token stream from the input text.
        /// </summary>
        /// <param name="text">Input text</param>
        public void Read(String text) {
            _pos = 0;
            _line = 1;
            _col = 1;
            _token.Clear();
            _currentToken = String.Empty;

            if(text == null) {
                throw new ArgumentNullException("text", "Text cannot be null");
            }
            _data = text;
        }

        /// <summary>
        /// Creates a new token stream from the input text.
        /// </summary>
        /// <param name="reader">Input text</param>
        public void Read(TextReader reader) {
            _pos = 0;
            _line = 1;
            _col = 1;
            _token.Clear();
            _currentToken = String.Empty;

            if(reader == null) {
                throw new ArgumentNullException("reader", "TextReader cannot be null");
            }
            _data = reader.ReadToEnd();
        }

        /// <summary>
        /// Checks if we have another token left in the token stream.
        /// </summary>
        /// <returns></returns>
        public bool HasNext() {
            //Takes care of first/last cases when we're starting out or after we're done, and will reveal
            //if we do have a token left
            if(String.IsNullOrEmpty(_currentToken)) {
                GoToNextToken();
            }
            if(GetCharacter(0) == EOF) {
                return false;
            }
            return true;
        }

        /// <summary>
        /// Peeks ahead to the next token and checks if it matches with the string sequence. 
        /// This does not advance the current position, and uses InvariantCultureIgnoreCase
        /// as the string comparison.
        /// </summary>
        /// <param name="token">String sequence to check</param>
        /// <returns>True if the next token matches</returns>
        public bool HasNext(String token) {
            return HasNext(token, StringComparison.InvariantCultureIgnoreCase);
        }

        /// <summary>
        /// Peeks ahead to the next token and checks if it matches with the string sequence. 
        /// This does not advance the current position.
        /// </summary>
        /// <param name="token">String sequence to check</param>
        /// <param name="comparison">Comparison to use</param>
        /// <returns>True if the next token matches</returns>
        public bool HasNext(String token, StringComparison comparison) {
            //If called more than once and NextToken() isn't called, we save the look ahead
            //so we don't have to keep collecting it.
            if(!String.IsNullOrEmpty(_peekToken)) {
                return _peekToken.Equals(token, comparison);
            }

            int lc = _line;
            int cc = _col;
            int pos = _pos;
            int slc = _savedLine;
            int scc = _savedCol;
            String sstr = _currentToken;

            String str = NextToken();
            bool result = false;

            if(!String.IsNullOrEmpty(str)) {
                result = str.Equals(token, comparison);
                _peekToken = str;
            }

            _line = lc;
            _col = cc;
            _pos = pos;
            _savedLine = slc;
            _savedCol = scc;
            _currentToken = sstr;

            return result;
        }

        /// <summary>
        /// Peeks ahead to the next token and checks if it starts with the string sequence. 
        /// This does not advance the current position, and uses InvariantCultureIgnoreCase
        /// as the string comparison.
        /// </summary>
        /// <param name="token">String sequence to check</param>
        /// <returns>True if the next token matches</returns>
        public bool HasNextStartsWith(String token) {
            return HasNextStartsWith(token, StringComparison.InvariantCultureIgnoreCase);
        }

        /// <summary>
        /// Peeks ahead to the next token and checks if it starts with the string sequence. 
        /// This does not advance the current position.
        /// </summary>
        /// <param name="token">String sequence to check</param>
        /// <param name="comparison">Comparison to use</param>
        /// <returns>True if the next token matches</returns>
        public bool HasNextStartsWith(String token, StringComparison comparison) {
            //If called more than once and NextToken() isn't called, we save the look ahead
            //so we don't have to keep collecting it.
            if(!String.IsNullOrEmpty(_peekToken)) {
                return _peekToken.StartsWith(token, comparison);
            }

            int lc = _line;
            int cc = _col;
            int pos = _pos;
            int slc = _savedLine;
            int scc = _savedCol;
            String sstr = _currentToken;

            String str = NextToken();
            bool result = false;

            if(!String.IsNullOrEmpty(str)) {
                result = str.StartsWith(token, comparison);
                _peekToken = str;
            }

            _line = lc;
            _col = cc;
            _pos = pos;
            _savedLine = slc;
            _savedCol = scc;
            _currentToken = sstr;

            return result;
        }

        /// <summary>
        /// Parses the next token as a boolean.
        /// </summary>
        /// <returns>Boolean</returns>
        public bool NextBool() {
            return bool.Parse(NextToken());
        }

        /// <summary>
        /// Parses the next token as an integer.
        /// </summary>
        /// <returns>Int</returns>
        public int NextInt() {
            String token = NextToken();
            if(token.StartsWith("0x", StringComparison.InvariantCulture)) {
                return int.Parse(token.Substring(2), NumberStyles.HexNumber);
            }
            return int.Parse(token);
        }

        /// <summary>
        /// Parses the next token as a float.
        /// </summary>
        /// <returns>Float</returns>
        public float NextSingle() {
            String str = NextToken();
            return float.Parse(str.TrimEnd(_numDelims), CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Gets the next token from the text. If reached the end of the text,
        /// an empty string is returned. Whitespaces, separators, comments,
        /// and control characters are ignored.
        /// </summary>
        /// <returns>The token</returns>
        public String NextToken() {
            while(true) {
                char c = GetCharacter(0);

                switch(c) {
                    case EOF:
                        _currentToken = String.Empty;
                        return _currentToken;
                    case ' ':
                    case '\t':
                    case ';':
                    case '\r':
                    case '\n':
                        GoToNextToken();
                        break;
                    case '/':
                        if(!GoToEndOfComment()) {
                            return ReadToken();
                        } else {
                            GoToNextToken();
                        }
                        break;
                    default:
                        return ReadToken();
                }
            }
        }

        /// <summary>
        /// Reads a complete token, returning when we hit
        /// either the end of the text, a separator, a comment,
        /// or a control character.
        /// </summary>
        /// <returns>The token</returns>
        private String ReadToken() {
            StartReading();
            Consume();

            while(true) {
                char c = GetCharacter(0);

                switch(c) {
                    case EOF:
                    case ' ':
                    case '\t':
                    case '\r':
                    case '\n':
                    case ';':
                        GoToNextToken();
                        _currentToken = _token.ToString();
                        return _currentToken;
                    case '/':
                        if(GoToEndOfComment()) {
                            GoToNextToken();
                            _currentToken = _token.ToString();
                            return _currentToken;
                        }
                        Consume();
                        break;
                    default:
                        Consume();
                        break;
                }
            }
        }

        /// <summary>
        /// Gets the character at the current position, or
        /// subsequent characters. Character will be located at pos + count
        /// </summary>
        /// <param name="count">Number of characters to skip ahead</param>
        /// <returns>Character at the position</returns>
        private char GetCharacter(int count) {
            if(_pos + count >= _data.Length) {
                return EOF;
            } else {
                return _data[_pos + count];
            }
        }

        /// <summary>
        /// Prepare to start reading a token.
        /// </summary>
        private void StartReading() {
            _token.Clear();
            _peekToken = null;
            _savedCol = _col;
            _savedLine = _line;
        }

        /// <summary>
        /// Consumes a single valid character, and adds it to the current token.
        /// </summary>
        private void Consume() {
            _token.Append(_data[_pos]); ;
            _pos++;
            _col++;
        }

        /// <summary>
        /// Skips an invalid character
        /// </summary>
        private void Skip() {
            _pos++;
        }

        /// <summary>
        /// Skips a number of invalid characters
        /// </summary>
        /// <param name="count">Number of characters to skip</param>
        private void Skip(int count) {
            _pos += count;
        }

        /// <summary>
        /// Go to the beginning of the very next token, skipping over
        /// separators, comments, and control characters.
        /// </summary>
        private void GoToNextToken() {
            while(true) {
                char c = GetCharacter(0);

                switch(c) {
                    case EOF:
                        Skip();
                        return;
                    case ' ':
                    case '\t':
                    case ';':
                        Skip();
                        _col++;
                        break;
                    case '\r':
                        char peek = GetCharacter(1);
                        if(peek == '\n') {
                            Skip();
                        }
                        Skip();
                        _line++;
                        _col = 1;
                        break;
                    case '\n':
                        Skip();
                        _line++;
                        _col = 1;
                        break;
                    case '/':
                        if(GoToEndOfComment()) {
                            GoToNextToken();
                        }
                        return;
                    default:
                        return;
                }
            }
        }

        /// <summary>
        /// Checks if we're at the start of a comment (//), and
        /// if we are skip all the characters until we reach a control character.
        /// </summary>
        /// <returns></returns>
        private bool GoToEndOfComment() {
            char dbSlash = GetCharacter(1);

            if(dbSlash == '/') {
                Skip(2);
                _col += 2;
            } else {
                return false;
            }

            while(true) {
                char c = GetCharacter(0);

                if(!(c == '\r' || c == '\n')) {
                    Skip();
                    _col++;
                } else {
                    return true;
                }
            }
        }
    }
}
